{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Example for anomaly detection with LSTM autoencoder architectures\n",
    "\n",
    "There is a multitude of successful architecture. In the following we demonstrate the implementation of 3 possible architecture types.\n",
    "\n",
    "## Models"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-09-28T08:11:59.977244Z",
     "iopub.status.busy": "2025-09-28T08:11:59.976923Z",
     "iopub.status.idle": "2025-09-28T08:12:03.514172Z",
     "shell.execute_reply": "2025-09-28T08:12:03.513390Z"
    }
   },
   "outputs": [],
   "source": [
    "from river import preprocessing, metrics, datasets\n",
    "\n",
    "from deep_river.anomaly import RollingAutoencoder\n",
    "from torch import nn, manual_seed\n",
    "import torch"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![](sutskever_ae.png)\n",
    "\n",
    "LSTM Encoder-Decoder architecture by Sutskever et al. 2014 (https://arxiv.org/abs/1409.3215). The decoder only gets access to its own prediction of the previous timestep. Decoding also takes performed backwards."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-09-28T08:12:03.516986Z",
     "iopub.status.busy": "2025-09-28T08:12:03.516701Z",
     "iopub.status.idle": "2025-09-28T08:12:03.526080Z",
     "shell.execute_reply": "2025-09-28T08:12:03.525346Z"
    }
   },
   "outputs": [],
   "source": [
    "class LSTMDecoder(nn.Module):\n",
    "    def __init__(\n",
    "        self,\n",
    "        input_size,\n",
    "        hidden_size,\n",
    "        sequence_length=None,\n",
    "        predict_backward=True,\n",
    "        num_layers=1,\n",
    "    ):\n",
    "        super().__init__()\n",
    "\n",
    "        self.cell = nn.LSTMCell(input_size, hidden_size)\n",
    "        self.input_size = input_size\n",
    "        self.hidden_size = hidden_size\n",
    "\n",
    "        self.predict_backward = predict_backward\n",
    "        self.sequence_length = sequence_length\n",
    "        self.num_layers = num_layers\n",
    "        self.lstm = (\n",
    "            None\n",
    "            if num_layers <= 1\n",
    "            else nn.LSTM(\n",
    "                input_size=hidden_size,\n",
    "                hidden_size=hidden_size,\n",
    "                num_layers=num_layers - 1,\n",
    "            )\n",
    "        )\n",
    "        self.linear = (\n",
    "            None\n",
    "            if input_size == hidden_size\n",
    "            else nn.Linear(hidden_size, input_size)\n",
    "        )\n",
    "\n",
    "    def forward(self, h, sequence_length=None):\n",
    "        \"\"\"Computes the forward pass.\n",
    "\n",
    "        Parameters\n",
    "        ----------\n",
    "        x:\n",
    "            Input of shape (batch_size, input_size)\n",
    "\n",
    "        Returns\n",
    "        -------\n",
    "        Tuple[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]\n",
    "            Decoder outputs (output, (h, c)) where output has the shape (sequence_length, batch_size, input_size).\n",
    "        \"\"\"\n",
    "\n",
    "        if sequence_length is None:\n",
    "            sequence_length = self.sequence_length\n",
    "        x_hat = torch.empty(sequence_length, h.shape[0], self.hidden_size)\n",
    "        for t in range(sequence_length):\n",
    "            if t == 0:\n",
    "                h, c = self.cell(h)\n",
    "            else:\n",
    "                input = h if self.linear is None else self.linear(h)\n",
    "                h, c = self.cell(input, (h, c))\n",
    "            t_predicted = -t if self.predict_backward else t\n",
    "            x_hat[t_predicted] = h\n",
    "\n",
    "        if self.lstm is not None:\n",
    "            x_hat = self.lstm(x_hat)\n",
    "\n",
    "        return x_hat, (h, c)\n",
    "\n",
    "\n",
    "class LSTMAutoencoderSutskever(nn.Module):\n",
    "    def __init__(self, n_features, hidden_size=30, n_layers=1):\n",
    "        super().__init__()\n",
    "        self.n_features = n_features\n",
    "        self.hidden_size = hidden_size\n",
    "        self.n_layers = n_layers\n",
    "        self.encoder = nn.LSTM(\n",
    "            input_size=n_features, hidden_size=hidden_size, num_layers=n_layers\n",
    "        )\n",
    "        self.decoder = LSTMDecoder(\n",
    "            input_size=hidden_size,\n",
    "            hidden_size=n_features,\n",
    "            predict_backward=True,\n",
    "        )\n",
    "\n",
    "    def forward(self, x):\n",
    "        _, (h, _) = self.encoder(x)\n",
    "        x_hat, _ = self.decoder(h[-1], x.shape[0])\n",
    "        return x_hat"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Testing\n",
    "\n",
    "The models can be tested with the code in the following cells. Since River currently does not feature any anomaly detection datasets with temporal dependencies, the results should be expected to be somewhat inaccurate.  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-09-28T08:12:03.528594Z",
     "iopub.status.busy": "2025-09-28T08:12:03.528368Z",
     "iopub.status.idle": "2025-09-28T08:12:04.426469Z",
     "shell.execute_reply": "2025-09-28T08:12:04.425628Z"
    }
   },
   "outputs": [],
   "source": [
    "_ = manual_seed(42)\n",
    "dataset = datasets.CreditCard().take(5000)\n",
    "metric = metrics.RollingROCAUC(window_size=5000)\n",
    "\n",
    "module = LSTMAutoencoderSutskever(30)  # Set this variable to your architecture of choice\n",
    "ae = RollingAutoencoder(module=module, lr=0.005)\n",
    "scaler = preprocessing.StandardScaler()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2025-09-28T08:12:04.429163Z",
     "iopub.status.busy": "2025-09-28T08:12:04.428881Z",
     "iopub.status.idle": "2025-09-28T08:12:26.335341Z",
     "shell.execute_reply": "2025-09-28T08:12:26.334666Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ROCAUC: 0.6839\n"
     ]
    }
   ],
   "source": [
    "for x, y in list(dataset):\n",
    "    scaler.learn_one(x)\n",
    "    x = scaler.transform_one(x)\n",
    "    score = ae.score_one(x)\n",
    "    metric.update(y_true=y, y_pred=score)\n",
    "    ae.learn_one(x=x, y=None)\n",
    "print(f\"ROCAUC: {metric.get():.4f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "deep-river",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
